Rozwiązania dla relacji zawierania

Kompozycja, czyli przechowywanie kolekcji obiektów danego typu wewnątrz obiektu zwanego kontenerem, to moim zdaniem najważniejszy ze związków między klasami. Jeśli chodzi o projektowanie aplikacji na wyższym poziomie, nie o implementowanie uniwersalnych kontenerów jak te z STL, pojawia się tu wiele możliwości. Postaram się je krótko przedstawić. Używam języka C++ i programowania obiektowego. Pomijam ewentualną możliwość układania obiektów w hierarchię (drzewo). Nie pomijam za to możliwości używania dziedziczenia i polimorfizmu dla klas elemenetów przechowywanych w kontenerze.

Nad problemem zastanawiałem się już od dawna, a do napisania tego tekstu bezpośrednio zainspirował mnie Xion notką na swoim blogu: Trzy rozwiązania dla relacji zawierania.

Tworzenie obiektów

Struktura

Jeśli elementy mają być małymi strukturami niosącymi tylko zbiór parametrów - bez własnego stanu, bez metod, bez wyliczanych i przechowywanych tymczasowo danych, bez polimforfizmu (albo z polimorfizmem zrealizowanym tylko za pomocą pola wyliczeniowego typu) - można to zrealizować jako strukturę. Wówczas użytkownik nie ma dostępu do elementów kolekcji w sensie bezpośrednim.

struct MATERIAL
{
  enum TYPE
  {
    TYPE_NORMAL,
    TYPE_SPECULAR,
  };
  TYPE Type;
  D3DXCOLOR Color;
  string Texture;
  float SpecularPower;
};

class MaterialCollection
{
public:
  // Tworzy materiał lub, jeśli taki już istnieje, zastępuje go
  void SetMaterial(const string &Name, const MATERIAL &Params);
  // Usuwa materiał, jeśli taki istnieje
  void DeleteMaterial(const string &Name);
  
  // Zwraca true, jeśli taki istnieje
  bool MaterialExists(const string &Name);
  // Zwraca parametry podanego materiału wpisując je do podanej struktury
  void GetMaterial(MATERIAL *Out_Params, const string &Name);
};

Metody kontenera do tworzenia elementów

To rozwiązanie wymaga znajomości pełnej listy polimorficznych typów elementów podczas implementowania kontenera. Kontener zawiera metody tworzące elemnty każdego typu na podstawie przekazanych parametrów.

class MaterialCollection
{
public:
  void CreateNormalMaterial(const string &Name, const D3DXCOLOR &Color, const string &Texture);
  void CreateSpecularMaterial(const string &Name, const D3DXCOLOR &Color, const string &Texture, float SpecularPower);
  void DeleteMaterial(const string &Name);
};

Metoda kontenera do dodawania elementów

Użytkownik musi sam utworzyć element konkretnego typu używając jego konstruktora i alokując go na stercie, a następnie nie zapomnieć dodać go do kontenera.

struct BaseMaterial
{
  D3DXCOLOR Color;
  string Texture;
};

struct NormalMaterial : public BaseMaterial
{
  NormalMaterial(const D3DXCOLOR &Color, const string &Texture);
};

struct SpecularMaterial : public BaseMaterial
{
  float SpecularPower;
  NormalMaterial(const D3DXCOLOR &Color, const string &Texture, float SpecularPower);
};

class MaterialCollection
{
public:
  void AddMaterial(BaseMaterial *Mat);
  void RemoveMaterial(BaseMaterial *Mat);
};

// Użycie:
BaseMaterial *m = new NormalMaterial(D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f), "Grass");
g_MaterialCollection->AddMaterial(m);

Element sam dodaje się do kontenera

Trzecie rozwiązanie daje możliwość po prostu tworzenia obiektów klasy elementu na stercie operatorem new, a one w swoim konstruktorze automatycznie dodają się, a w destruktorze automatycznie usuwają się z kolekcji. To rozwiązanie wydaje się najwygodniejsze i najbardziej odporne na błędy programisty-użytkownika.

Nie przeszkadza również fakt, że kontenerów może być wiele. Obiekt elementu może wymagać wówczas podania do konstruktora obiektu klasy kontenera, do której ma się dodać. Tak robi biblioteka Qt i wszelkie inne biblioteki do GUI.

Identyfikacja obiektów

Do obiektów przechowywanych już w kolekcji można się odnosić na różne sposoby. Można też te sposoby ze sobą łączyć, tzn. zapewniać więcej niż jeden spośród nich.

Wskaźnik

Najprostszy sposób to odnoszenie się bezpośrednio do elementów poprzez wskaźniki do obiektów. To jest najszybsze, ale ma też wady. Jakiś obiekt który chce dostać się do elementu kolekcji musi otrzymać od kogoś wskaźnik do tego elementu, gdyż sama kolekcja nie jest w stanie mu go zwrócić. Poza tym odniesienie się pod wskaźnik do obiektu który nie istnieje czy też właśnie został usunięty powoduje nieuchronny błąd programu i trudno sobie wyobrazić rozwiązanie tego problemu przy identyfikacji elementów przez wskaźniki (chyba że każdorazowo użyjemy metody kontenera sprawdzającej, czy element o podanym wskaźniku istnieje).

Identyfikator generowany

Identyfikator liczbowy (uchwyt) wprowadza poziom pośredniości umożliwiający dostęp do elementów kontenera (wyszukiwanie) znając identyfikator. Identyfikatory elementów są przy tym nadawane przez kontener podczas dodawania jako przypadkowe (prawdopodobnie kolejne) liczby, toteż trzeba je po otrzymaniu pamiętać tak jakby trzeba było pamiętać wskaźnik - niewielka korzyść.

Identifikator własny

Można zapewnić możliwość nadawania obiektom własnych, wybranych identyfikatorów liczbowych. Dzięki temu można się "umówić" (poprzez wartość zapisywaną wprost w kodzie, poprzez stałą albo nawet wartość wczytywaną z pliku), że dana liczba identyfikuje dany obiekt i w ten sposób odnosić się do niego z różnych miejsc w kodzie. Problemem, jaki tu powstaje jest możliwy konflikt identyfikatorów. Trzeba go rozwiązać ręcznie, np. zakładając, że obiekty pewnej grupy będą nosiły kolejne identyfikatory 1001, 1002, 1003, obiekty innej 2001, 2002, 2003 itp.

Nazwa

Jako nadawane we własnym zakresie identyfikatory używać można nie liczb, ale łańcuchów znaków. Takie nazwy są bardziej czytelne, ale wyszukiwanie obiektów w kontenerze wg nazwy jest dużo wolniejsze.

Rozwiązania mieszane

Można zapewnić więcej niż jedno z tych rozwiązań na raz. Można na przykład nadawać obiektom nazwy łańcuchowe (co zapewnia czytelność), ale na ich podstawie móc pobierać z kontenera bezopśredni wskaźnik do obiektu (co zapewnia szybkość). W ten sposób można wskaźnik pobierać na podstawie nazwy tylko przy pierwszym użyciu i dalej go przechowywać już jako wskaźnik.

Adam Sawicki
regedit.gamedev.pl
25 lipca - 3 sierpnia 2007